/**********************************************************************

 Audacity: A Digital Audio Editor

 RealtimeEffectManager.cpp

 Paul Licameli split from EffectManager.cpp

 **********************************************************************/
#include "RealtimeEffectManager.h"
#include "RealtimeEffectState.h"
#include "Channel.h"

#include <memory>
#include "Project.h"

#include <atomic>
#include <wx/time.h>

static const AttachedProjectObjects::RegisteredFactory manager
{
    [](AudacityProject& project)
    {
        return std::make_shared<RealtimeEffectManager>(project);
    }
};

RealtimeEffectManager& RealtimeEffectManager::Get(AudacityProject& project)
{
    return project.AttachedObjects::Get<RealtimeEffectManager&>(manager);
}

const RealtimeEffectManager&
RealtimeEffectManager::Get(const AudacityProject& project)
{
    return Get(const_cast<AudacityProject&>(project));
}

RealtimeEffectManager::RealtimeEffectManager(AudacityProject& project)
    : mProject(project)
{
}

RealtimeEffectManager::~RealtimeEffectManager()
{
}

bool RealtimeEffectManager::IsActive() const noexcept
{
    return mActive;
}

void RealtimeEffectManager::Initialize(
    RealtimeEffects::InitializationScope& scope,
    unsigned numPlaybackChannels,
    double sampleRate, size_t audioThreadBufferSize)
{
    // (Re)Set processor parameters
    mRates.clear();
    mGroups.clear();

    // RealtimeAdd/RemoveEffect() needs to know when we're active so it can
    // initialize newly added effects
    mActive = true;

    // Tell each state to get ready for action
    VisitAll([&scope, sampleRate, audioThreadBufferSize](RealtimeEffectState& state, bool) {
        scope.mInstances.push_back(state.Initialize(sampleRate, audioThreadBufferSize));
    });

    // Leave suspended state
    SetSuspended(false);

    VisitGroup(MasterGroup, [&](RealtimeEffectState& state, bool) {
        scope.mInstances.push_back(state.AddGroup(MasterGroup, numPlaybackChannels, sampleRate, audioThreadBufferSize));
    });
}

void RealtimeEffectManager::AddGroup(
    RealtimeEffects::InitializationScope& scope,
    const ChannelGroup& group, unsigned chans, float rate, size_t audioThreadBufferSize)
{
    mGroups.push_back(&group);
    mRates.insert({ &group, rate });

    VisitGroup(&group,
               [&](RealtimeEffectState& state, bool) {
        scope.mInstances.push_back(state.AddGroup(&group, chans, rate, audioThreadBufferSize));
    }
               );
}

void RealtimeEffectManager::Finalize() noexcept
{
    // Reenter suspended state
    SetSuspended(true);

    VisitAll([](RealtimeEffectState& state, bool){ state.Finalize(); });

    // Reset processor parameters
    mGroups.clear();
    mRates.clear();

    // No longer active
    mActive = false;
}

//
// This will be called in a different thread than the main GUI thread.
//
void RealtimeEffectManager::ProcessStart(bool suspended)
{
    // Can be suspended because of the audio stream being paused or because
    // effects have been suspended.
    VisitAll([suspended](RealtimeEffectState& state, bool listIsActive){
        state.ProcessStart(!suspended && listIsActive);
    });
}

// This will be called in a thread other than the main GUI thread.
//
size_t RealtimeEffectManager::Process(bool suspended,
                                      const ChannelGroup* group,
                                      float* const* buffers, float* const* scratch, float* const dummy,
                                      unsigned nBuffers, size_t numSamples)
{
    // Can be suspended because of the audio stream being paused or because
    // effects have been suspended, so allow the samples to pass as-is.
    if (suspended) {
        return 0;
    }

    // Allocate the in and out buffer arrays
    const auto ibuf
        =static_cast<float**>(alloca(nBuffers * sizeof(float*)));
    const auto obuf
        =static_cast<float**>(alloca(nBuffers * sizeof(float*)));

    // And populate the input with the buffers we've been given while allocating
    // NEW output buffers
    for (unsigned int i = 0; i < nBuffers; i++) {
        ibuf[i] = buffers[i];
        obuf[i] = scratch[i];
    }

    // Now call each effect in the chain while swapping buffer pointers to feed
    // the output of one effect as the input to the next effect
    // Tracks how many processors were called
    size_t called = 0;
    size_t totalDiscardable = 0;
    VisitGroup(group,
               [&](RealtimeEffectState& state, bool)
    {
        const size_t discardable = std::min(state.Process(group, nBuffers, ibuf, obuf, dummy, numSamples), numSamples);
        for (unsigned int i = 0; i < nBuffers; i++) {
            ibuf[i] += discardable;
            obuf[i] += discardable;
        }
        numSamples -= discardable;
        totalDiscardable += discardable;
        for (auto i = 0; i < nBuffers; ++i) {
            std::swap(ibuf[i], obuf[i]);
        }
        called++;
    });

    // Once we're done, we might wind up with the last effect storing its results
    // in the temporary buffers.  If that's the case, we need to copy it over to
    // the caller's buffers.  This happens when the number of effects processed
    // is odd.
    if (called & 1) {
        for (unsigned int i = 0; i < nBuffers; i++) {
            memcpy(buffers[i], ibuf[i], numSamples * sizeof(float));
        }
    }

    //
    // This is wrong...needs to handle tails
    //
    return totalDiscardable;
}

//
// This will be called in a different thread than the main GUI thread.
//
void RealtimeEffectManager::ProcessEnd(bool suspended) noexcept
{
    // Can be suspended because of the audio stream being paused or because
    // effects have been suspended.
    VisitAll([suspended](RealtimeEffectState& state, bool){
        state.ProcessEnd();
    });
}

std::shared_ptr<RealtimeEffectState>
RealtimeEffectManager::MakeNewState(
    RealtimeEffects::InitializationScope* pScope,
    ChannelGroup* pGroup, const PluginID& id)
{
    if (!pScope && mActive) {
        return nullptr;
    }
    auto pNewState = RealtimeEffectState::make_shared(id);
    auto& state = *pNewState;
    if (pScope && mActive) {
        // Adding a state while playback is in-flight
        auto pInstance = state.Initialize(pScope->mSampleRate, pScope->mAudioThreadBufferSize);
        pScope->mInstances.push_back(pInstance);

        if (pGroup == MasterGroup) {
            auto pInstance2 = state.AddGroup(MasterGroup, pScope->mNumPlaybackChannels, pScope->mSampleRate, pScope->mAudioThreadBufferSize);
            if (pInstance2 != pInstance) {
                pScope->mInstances.push_back(pInstance2);
            }
        } else {
            for (const auto group : mGroups) {
                if (pGroup != group) {
                    continue;
                }
                auto pInstance2
                    =state.AddGroup(group, pScope->mNumPlaybackChannels, mRates[group], pScope->mAudioThreadBufferSize);
                if (pInstance2 != pInstance) {
                    pScope->mInstances.push_back(pInstance2);
                }
            }
        }
    }
    return pNewState;
}

namespace {
RealtimeEffectList&
FindStates(AudacityProject& project, ChannelGroup* pGroup)
{
    return pGroup
           ? RealtimeEffectList::Get(*pGroup)
           : RealtimeEffectList::Get(project);
}
}

std::shared_ptr<RealtimeEffectState> RealtimeEffectManager::AddState(
    RealtimeEffects::InitializationScope* pScope,
    ChannelGroup* pGroup, const PluginID& id)
{
    auto& states = FindStates(mProject, pGroup);
    auto pState = MakeNewState(pScope, pGroup, id);
    if (!pState) {
        return nullptr;
    }

    // Only now add the completed state to the list, under a lock guard
    if (!states.AddState(pState)) {
        return nullptr;
    }
    Publish({
        RealtimeEffectManagerMessage::Type::EffectAdded,
        pGroup ? pGroup : MasterGroup
    });
    return pState;
}

std::shared_ptr<RealtimeEffectState> RealtimeEffectManager::ReplaceState(
    RealtimeEffects::InitializationScope* pScope,
    ChannelGroup* pGroup, size_t index, const PluginID& id)
{
    auto& states = FindStates(mProject, pGroup);
    auto pOldState = states.GetStateAt(index);
    if (!pOldState) {
        return nullptr;
    }
    auto pNewState = MakeNewState(pScope, pGroup, id);
    if (!pNewState) {
        return nullptr;
    }

    // Only now swap the completed state into the list, under a lock guard
    if (!states.ReplaceState(index, pNewState)) {
        return nullptr;
    }
    if (mActive) {
        pOldState->Finalize();
    }
    Publish({
        RealtimeEffectManagerMessage::Type::EffectReplaced, pGroup
    });
    return pNewState;
}

void RealtimeEffectManager::RemoveState(
    RealtimeEffects::InitializationScope* pScope,
    ChannelGroup* pGroup,
    const std::shared_ptr<RealtimeEffectState> pState)
{
    auto& states = FindStates(mProject, pGroup);

    // Remove the state from processing (under the lock guard) before finalizing
    states.RemoveState(pState);
    if (mActive) {
        pState->Finalize();
    }
    Publish({
        RealtimeEffectManagerMessage::Type::EffectRemoved,
        pGroup ? pGroup : MasterGroup
    });
}

std::optional<size_t> RealtimeEffectManager::FindState(
    ChannelGroup* pGroup,
    const std::shared_ptr<RealtimeEffectState>& pState) const
{
    auto& states = FindStates(mProject, pGroup);
    return states.FindState(pState);
}
